/*
 * 代号：凤凰
 * http://www.jphenix.org
 * 2014-06-04
 * V4.0
 */
package com.jphenix.driver.log.xlogc;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.jphenix.driver.cluster.ClusterFilter;
import com.jphenix.driver.cluster.ServerInfoVO;
import com.jphenix.kernel.objectloader.instanceb.BaseBean;
import com.jphenix.servlet.filter.BaseFilter;
import com.jphenix.share.lang.SBoolean;
import com.jphenix.share.lang.SDate;
import com.jphenix.share.lang.SListMap;
import com.jphenix.share.lang.SString;
import com.jphenix.share.tools.JarTools;
import com.jphenix.share.util.BaseUtil;
import com.jphenix.share.util.SFilesUtil;
import com.jphenix.standard.beans.ITriggerInit;
import com.jphenix.standard.docs.BeanInfo;
import com.jphenix.standard.docs.ClassInfo;
import com.jphenix.standard.log.ILogShadow;
import com.jphenix.standard.servlet.IFilter;
import com.jphenix.standard.servlet.IRequestManager;
import com.jphenix.standard.servlet.IResponseManager;
import com.jphenix.standard.servlet.api.IFilterConfig;

/**
 * 文件日志影子类
 * 
 * 输出日志增加了同步关键字
 * 
 * 2018-06-04 修改了备份日志包的扩展文件名，由jar变为zip
 * 2018-07-12 增加了输出内部调试日志相关的属性方法
 * 2018-07-25 在终止方法中，增加了终止检查token超时线程方法
 * 2018-09-13 简化了整个架构中日志初始化步骤
 * 2018-12-14 故障码接收部分，屏蔽了无效故障码信息
 * 2019-01-08 弃用了invalidServer，请看变量ServerInfoVO.invalidServer声明处的注释
 * 2019-03-15 完善了输出自定义日志方法
 * 2019-03-27 解决了无法控制输出sql不写入日志文件的错误
 * 2019-03-29 解决了远程无法查看操作数据库的日志，并且修改了如果不显示日志，在远程控制台也不会显示日志（除了debug日志）
 * 2019-06-15 按照IFilter增加了过滤器初始化方法
 * 2019-06-27 增加了参数值为空判断
 * 2019-07-15 增加了自定义日志根路径
 * 2020-03-13 修改了从URL中获取请求参数
 * 2020-08-03 修改了响应请求逻辑，无论前面有几层路径，只要末尾是/_log.ha，都能响应到
 * 2020-08-20 按照修改后的接口，将日志编码格式拆分成输出控制台日志编码格式和写文件日志编码格式
 * 2020-08-30 设置输出报文头内容格式，否则浏览器会报解析XML错误
 * 2021-03-16 日志编码格式固定为UTF-8再从系统取。整理了代码格式
 * 2022-07-11 写入启动日志文件的同时，也将内容写入普通日志文件
 * 2022-09-04 隔离了ServletApi，兼容新老Tomcat
 * 2024-03-27 适配了JDK17
 * 2024-07-16 修改了获取日志逻辑，避免短时间内大量日志导致浏览器无响应。另外提高了处理效率
 * 2024-08-17 之前无脑修改，把故障码功能弄坏了，已经修复好了
 * 
 * 注意：日志写文件用的字符编码为 sun.jnu.encoding 变量值，通常该值跟操作系统相同
 * 
 * com.jphenix.driver.log.xlogc.XLogFilter
 * @author 刘虻
 * 2009-12-4 下午05:04:00
 */
@BeanInfo({"logshadow"})
@ClassInfo({"2024-08-17 23:16","文件日志影子类"})
public class XLogFilter extends BaseBean implements IFilter,ITriggerInit,ILogShadow {

  private String 
    consoleEncoding,                             //控制台日志编码
    fileEncoding,                                //文件日志编码
    filePath;                                    //文件路径（绝对路径）

  private boolean
    outLog                  = true,               //是否输出普通日志
    outWarning              = true,               //是否输出警告日志
    outStart                = true,               //是否输出启动日志
    outError                = true,               //是否输出错误日志
    outSql                  = true,               //是否输出数据语句日志
    outRuntime              = true,               //是否输出耗费时间日志
    outSystemLog            = true,               //是否输出系统普通日志
    outSystemError          = true,               //是否输出系统错误日志
    outInfo                 = true,               //是否输出info日志
    outData                 = true,               //是否输出交易数据日志
    outTLog                 = true,               //是否输出临时日志（调试后需要删除的日志）
  
    writeLog                = false,              //是否写入普通日志
    writeWarning            = false,              //是否写入警告日志
    writeError              = false,              //是否写入错误日志
    writeStart              = false,              //是否写入启动日志
    writeSql                = false,              //是否写入数据语句
    writeRuntime            = false,              //是否写入运行时间
    writeSystemLog          = false,              //是否写入系统普通日志
    writeSystemError        = false,              //是否写入系统错误日志
    writeInfo               = false,              //是否写入info日志
    writeDebug              = false,              //是否写入调试日志
    writeData               = false,              //是否写入交易数据日志
    writeTLog               = false;              //是否写入临时日志（调试后需要删除的日志）
    
  private JarTools jarTools = null;                            //打包工具
  private File 
    logFile,                                      //普通日志文件对象
    startFile,                                    //启动日志文件对象
    dataFile                                      //交易数据文件对象
              = null;
  private PrintStream 
    logPris,                                      //普通日志输出流对象
    startPris,                                    //启动日志输出流对象
    dataPris                                      //交易数据输出流对象
              = null;

  private BackupLogThread   blt;                                              //备份线程
  private long              lastTime      = 0;                                //构造当天的最后时间
  private PrintStream       systemOut     = null;                             //系统输出日志流
  private PrintStream       systemErr     = null;                             //系统输出错误信息流
  private ErrorFilterStream efs           = null;                             //错误信息过滤输出流
  private ClusterFilter     cf            = null;                             //集群管理类
  private String            serverName    = null;                             //当前服务器名
  private String            groupName     = null;                             //当前服务器所属分组名
  private String            servletPath   = "/_log.ha";                       //获取日志响应动作
  private SListMap<Long>    callerMap     = new SListMap<Long>();             //调用者信息容器
  private long              callerOutTime = 30000;                            //请求查看实时日志的超时时间
  private LogEventAroundVO  leaVO         = new LogEventAroundVO();           //日志信息缓存容器

  private List<String>      alertCodeList      = new ArrayList<String>();     //保存错误代码
  private Map<String,Long>  alertBeforeTimeMap = new HashMap<String,Long>();  //发生开始时间
  private Map<String,Long>  alertAfterTimeMap  = new HashMap<String,Long>();  //最后发生时间
  private Map<String,Long>  alertCountMap      = new HashMap<String,Long>();  //发生累计数量

  private SListMap<PrintStream> psMap          = new SListMap<PrintStream>(); //自定义输出流容器
  private Map<String,File>      fileMap        = new HashMap<String,File>();  //自定义日志文件对象
  private List<String>          logFileKeyList = new ArrayList<String>();           //日志文件头序列（存在其中的才允许写日志）

  //时间格式处理类
  private static final SimpleDateFormat simpleDateFormat = 
    new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    
  private CheckTimeOut cto = null;                                       //检测申请实时查看日志会话超时线程

  /**
   * 检测申请实时查看日志会话超时线程
   * @author MBG
   * 2018年7月12日
   */
  private class CheckTimeOut extends Thread {
    
    /**
     * 构造函数
     * @author MBG
     */
    public CheckTimeOut() {
      super("XLogFilter-CheckTimeOut");
    }
    
    /**
     * 覆盖函数
     */
    @Override
      public void run() {
      long         curTime;        //当前毫秒值
      List<String> removeKeyList;  //准备移除的会话主键序列
      while(true) {
        if(callerMap.size()>0) {
          curTime = System.currentTimeMillis();
          removeKeyList = new ArrayList<String>();
          for(int i=0;i<callerMap.size();i++) {
            if(callerMap.get(i)<curTime) {
              removeKeyList.add(callerMap.getKey(i));
            }
          }
          for(String key:removeKeyList) {
            callerMap.remove(key);
          }
        }
        try {
          Thread.sleep(5);
        }catch(Exception e) {
          break;
        }
      }
    }
  }

  /**
   * 构造函数
   * @author 刘虻
   */
  public XLogFilter() {
    super();
  }

  /**
   * 日志备份线程
   * @author 刘虻 
   * 2007-2-1  下午04:48:51
   */
  protected class BackupLogThread extends Thread {

    protected boolean isRun = false; //是否运行中
      
    /**
     * 构造函数 
     * @author 刘虻
     * 2007-2-1 下午04:50:06
     */
    public BackupLogThread() {
      super();
      setName("XLog-BackupLogThread");
    }

    /**
     * 覆盖方法
     * @author 刘虻
     * 2007-2-2下午12:46:11
     */
    public void destroy() {
        isRun =false;
    }

    /**
     * 覆盖方法 
     * @author  刘虻
     * 2007-2-1 下午04:50:16
     */
    @Override
    public void run() {
      isRun = true;
      while(isRun) {
        if (lastTime==0) {
          //初始化当天的最后时间
          lastTime = (new SDate((new SDate()).getDayEndString())).getMillisecond();
        }
        if (lastTime+1000<System.currentTimeMillis()) {
          //执行当日备份日志
          backupLogFile("log",getNewLogBakFileName("log"));
          //备份交易数据日志，如果存在的话
          backupDataLogFile();
          //执行自定义日志备份
          String fileKey; //文件头名
          for(int i=0;i<psMap.size();i++) {
            fileKey = psMap.getKey(i);
            backupLogFile(fileKey,getNewLogBakFileName(fileKey));
          }
        }
        try {
          Thread.sleep(30000); //间隔5分钟检查
        }catch(Exception e) {
          e.printStackTrace();
          break;
        }
      }
    }
  }

  /**
   * 初始化
   * 刘虻
   * 2009-12-7 上午10:55:53
   */
  @Override
  public void init() {
    //启动检测申请实时查看日志的会话超时线程
    cto = new CheckTimeOut();
    cto.start();
    if(consoleEncoding==null || consoleEncoding.length()<1) {
      //注意：不在获取系统编码格式，而是采用统一的UTF-8格式。如果在windows控制台显示乱码，不妨在启动脚本开始处增加语句：chcp 65001
      //consoleEncoding = SString.valueOf(System.getProperty("sun.jnu.encoding"));
      consoleEncoding = "UTF-8";
    }
    if(fileEncoding==null || fileEncoding.length()<1) {
      //fileEncoding = SString.valueOf(System.getProperty("file.encoding"));
      fileEncoding = "UTF-8";
    }
    systemOut = System.out;
    //重定向系统信息输出类
    System.setOut(
      new LogPrintStream(
        System.out
        ,this
        ,isOutSystemLog()
        ,isWriteSystemLog()));
    systemErr = System.err;
    //重定向系统错误输出类
    efs =  
      new ErrorFilterStream(
        System.err
        ,this
        ,isOutSystemError()
        ,isWriteSystemError());
    System.setErr(efs);
    //构建备份线程
    blt = new BackupLogThread();
    blt.start();
  }

  /**
   * 执行初始化
   * @param fe         过滤器管理类
   * @param config     Servlet配置信息类
   * @throws Exception 异常（如果初始化发生异常，则放弃不再使用）
   * 2019年6月15日
   * @author MBG
   */
  @Override
  public void init(BaseFilter bf, IFilterConfig config) throws Exception {}

  /**
   * 终止备份线程
   * 刘虻
   * 2010-9-10 下午04:46:05
   * @deprecated
   */
  @Override
  public void destroy() {
    try {
      blt.stop();
    }catch(Exception e) {}
    blt = null;
    try {
      cto.stop();
    }catch(Exception e) {}
    cto = null;
  }

  /**
   * 写日志
   * 刘虻
   * 2009-12-7 上午10:31:01
   * @param content 日志内容
   */
  @Override
  public synchronized void write(
    String  content,
    boolean showLog,
    boolean writeLog,
    boolean nativeLog) {
    if(showLog) {
      writeLogEvent(content);
      if(nativeLog) {
        return;
      }
      try {
        systemOut.write(content.getBytes(consoleEncoding));
      }catch(Exception e) {}
    }
    if(writeLog && !nativeLog) {
      getLogPrintStream().println(content);
    }
  }

  /**
   * 写日志到指定文件
   * @param content 日志内容
   * @param showLog 是否输出到控制台
   * @param fileKey 文件名（不带扩展名）
   * 2018年7月27日
   * @author MBG
   */
  @Override
  public void write(String content, boolean showLog, String fileKey) {
    if(showLog) {
      writeLogEvent(content);
        try {
          systemOut.write(content.getBytes(consoleEncoding));
        }catch(Exception e) {}
    }
    if(!isWriteSql()) {
      return;
    }
    if(fileKey==null ||fileKey.length()<1) {
      getLogPrintStream().println(content);
      return;
    }
    //获取对应的文件流
    PrintStream ps = getPrintStream(fileKey);
    if(ps!=null) {
      try {
        ps.write(content.getBytes(fileEncoding));
      }catch(Exception e) {}
    }
  }

  /**
   * 写日志
   * 刘虻
   * 2010-8-24 上午11:09:33
   * @param bytes 日志内容
   */
  @Override
  public synchronized void write(byte[] bytes) {
    try {
      getLogPrintStream().write(bytes);
    }catch(Exception e) {}
  }

  /**
   * 获取指定输出流
   * @param fileKey 文件名
   * @return        对应的文件输出流
   * 2018年7月27日
   * @author MBG
   */
  private synchronized PrintStream getPrintStream(String fileKey) {
    if(filePath==null || !logFileKeyList.contains(fileKey)) {
      return null;
    }
    //先从容器中获取输出流
    PrintStream ps = psMap.get(fileKey);
    if(ps==null) {
      backupLogFile(fileKey,null); //保证和备份同步
      //获得日志文件对象
      File file = SFilesUtil.createFile(filePath+"/"+fileKey+".txt");
      try {
        ps = 
          new PrintStream(
            new FileOutputStream(file,true)
            ,true
            ,fileEncoding);
        psMap.put(fileKey,ps);
        fileMap.put(fileKey,file);
      }catch(Exception e) {
        e.printStackTrace();
      }
    }
    return ps;
  }

  /**
   * 覆盖方法
   * 刘虻
   * 2010-8-24 下午12:48:57
   */
  @Override
  public void writeStart(
    String  content,
    boolean showLog,
    boolean writeLog) {
    if(showLog) {
      try {
        systemOut.write(content.getBytes(consoleEncoding));
      }catch(Exception e) {}
    }
    if(writeLog) {
      getStartPrintStream().println(content);
      // 不能只将日志放到启动日志文件中，也要放到普通日志文件中，
      // 因为启动日志文件在每次启动时，都会覆盖上一次的启动信息
      getLogPrintStream().println(content);
    }
  }

  /**
   * 输出（写入）交易数据日志
   * @param content  日志内容
   * @param showLog  是否显示日志到控制台
   * @param writeLog 是否写入日志到独立文件
   * 2016年6月7日
   * @author MBG
   */
  @Override
  public void writeData(String content, boolean showData, boolean writeData) {
    if(showData) {
      writeLogEvent(content);
      try {
        systemOut.write(content.getBytes(consoleEncoding));
      }catch(Exception e) {}
    }
    if(writeData) {
      getDataPrintStream().println(content);
    }
  }

  /**
   * 获得普通日志输出流对象
   * @author 刘虻
   * @return
   * 2005-12-18  21:10:12
   */
  protected PrintStream getLogPrintStream() {
    backupLogFile("log",null); //保证和备份同步
    if(logPris==null || logFile==null) {
      //获得日志文件对象
      logFile = SFilesUtil.createFile(filePath+"/log.txt");
      try {
        logPris = 
          new PrintStream(
            new FileOutputStream(logFile,true),
            true,
            fileEncoding);
      }catch(Exception e) {
        e.printStackTrace();
      }
    }
    return logPris;
  }

  /**
   * 获得启动日志输出流对象
   * 刘虻
   * 2009-12-7 上午11:17:30
   * @return 启动日志输出流对象
   */
  protected PrintStream getStartPrintStream() {
    if(startPris==null || startFile==null) {
      //获得日志文件对象
      startFile = SFilesUtil.createFile(filePath+"/start.txt");
      if(startFile.exists()) {
        startFile.delete();
      }
      try {
        startPris = 
          new PrintStream(
            new FileOutputStream(startFile,true),
            true,
            fileEncoding);
      }catch(Exception e) {
        e.printStackTrace();
      }
    }
    return startPris;
  }

  /**
   * 获取交易数据日志输出流
   * @return 交易数据日志输出流
   * 2016年6月7日
   * @author MBG
   */
  protected PrintStream getDataPrintStream() {
    if(dataPris==null || dataFile==null) {
      //获取日志文件对象
      dataFile = SFilesUtil.createFile(filePath+"/data_"+SDate.nowThinDate()+".txt");
      try {
        dataPris = 
          new PrintStream(
            new FileOutputStream(dataFile,true),
            true,
            fileEncoding);
      }catch(Exception e) {
        e.printStackTrace();
      }
    }
    return dataPris;
  }

  /**
   * 手动执行备份日志
   * 2015年5月22日
   * @author 马宝刚
   */
  @Override
  public void backupLogFile() {
    backupLogFile("log",getNewLogBakFileName("log"));
  }

  /**
   * 检测是否执行备份
   * @author 刘虻
   * 2007-12-11上午10:09:07
   * @param fileName 日志文件名
   * @param isSub 是否为当日日志片段备份
   */
  protected synchronized void backupLogFile(String fileKey,File reNameFile) {
    if (reNameFile==null) {
      if(psMap.get(fileKey)==null && logFileKeyList.contains(fileKey)) {
        //构建自定义文件对象
        File file = new File(filePath+"/"+fileKey+".txt");
        fileMap.put(fileKey,file);
        if(file.exists()) {
          backupLogFile(fileKey,getNewLogBakFileName(fileKey));
        }
      }else if (logFile==null) {
        //首次运行时,如果发现已存在日志文件,先备份之
        logFile = new File(filePath+"/"+fileKey+".txt");
        if (logFile.exists()) {
          backupLogFile(fileKey,getNewLogBakFileName(fileKey));
        }
        }
        return;
    }
    //日志文件全路径
    String logPath = filePath+"/"+fileKey+".txt";
    if(logFileKeyList.contains(fileKey)) {
      //获取指定输出流
      PrintStream ps = psMap.remove(fileKey);
      if(ps!=null) {
        try {
          ps.close();
        }catch(Exception e) {}
        ps = null;
      }
      //日志日志流对应的文件对象
      File file = fileMap.remove(fileKey);
      if(file!=null) {
        file.renameTo(reNameFile);
      }
      try {
        //备份文件名
        String jarFileName = reNameFile.getPath();
        jarFileName = SFilesUtil.getFilePath(jarFileName,false)+
          SFilesUtil.getFileBeforeName(jarFileName)+".zip";
        //压缩备份
        getjarTools().doJarThread(
          reNameFile.getPath(),
          SFilesUtil.getFilePath(reNameFile.getPath(),false),
          jarFileName,
          false,
          true);
        }catch(Exception e) {
        e.printStackTrace(systemErr);
        }
    }else {
      if (logFile==null) {
        logFile = new File(logPath);
      }
      if (logFile.length()>0) {
        //关闭日志流
        if(logPris!=null) {
          try {
            logPris.close();
          }catch(Exception e) {}
        }
        //重命名文件
        logFile.renameTo(reNameFile);
        try {
          //备份文件名
          String jarFileName = reNameFile.getPath();
          jarFileName = SFilesUtil.getFilePath(jarFileName,false)+
            SFilesUtil.getFileBeforeName(jarFileName)+".zip";
          //压缩备份
          getjarTools().doJarThread(
            reNameFile.getPath(),
            SFilesUtil.getFilePath(reNameFile.getPath(),false),
            jarFileName,
            false,
            true);
        }catch(Exception e) {
          e.printStackTrace(systemErr);
        }
        //重新建立文件
        logFile = new File(logPath);
        //清空流
        logPris = null;
      }
    }
  }

  /**
   * 检测是否执行备份
   * @author 刘虻
   * 2007-12-11上午10:09:07
   * @param fileName 日志文件名
   * @param isSub 是否为当日日志片段备份
   */
  protected void backupDataLogFile() {
    //正在使用的交易数据日志文件
    String dataLogFilePath = filePath+"/data_"+(new SDate()).setDayApoint(-1).getThinYMDString()+".txt";
    if(!(new File(dataLogFilePath)).exists()) {
      //正在使用的日志文件不存在，通常不会走到这一步
      return;
    }
    //关闭流，随后会在另外的方法中自动构建新的流
    if(dataPris!=null) {
      synchronized(dataPris) {
        try {
          dataPris.close();
        }catch(Exception e) {}
        dataPris = null;
        dataFile = null;
      }
    }
    //执行备份
    try {
      //备份文件名
      String jarFileName = 
        SFilesUtil.getFilePath(dataLogFilePath,false)+
          SFilesUtil.getFileBeforeName(dataLogFilePath)+".zip";
      //压缩备份
      getjarTools().doJarThread(
        dataLogFilePath,
        SFilesUtil.getFilePath(dataLogFilePath,false),
        jarFileName,
        false,
        true);
    }catch(Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * 获取有效的打包工具
   * @author 刘虻
   * 2007-1-6下午03:28:48
   * @return 有效的打包工具
   */
  protected JarTools getjarTools() {
    if (jarTools==null) {
      jarTools = new JarTools();
      //设置文件工具类
    }
    return jarTools;
  }

  /**
   * 获取新的日志备份文件
   * @author 刘虻
   * 2007-12-11上午10:14:11
   * @return 新的日志备份文件
   */
  protected File getNewLogBakFileName(String fileKey) {
    //日志文件全路径
    String logPath = filePath+"/"+fileKey+".txt";
    if (lastTime==0) {
      lastTime = (new SDate((new SDate()).getDayEndString())).getMillisecond();
    }
    int point = logPath.lastIndexOf("/");
    if(point<1) {
      point = logPath.lastIndexOf("\\");
    }
    if(point<1) {
      return null;
    }
    int fileNo = 0; //文件流水号
    //备份文件路径
    String backupPath = 
      logPath.substring(0,point+1)+
      fileKey+"_"+(new SDate(lastTime)).getDateNumberString()+"_";
    //构建返回值
    File reFile = new File(backupPath+fileNo+".txt");
    //备份文件
    File jarFile = new File(backupPath+fileNo+".zip");
    while(reFile.exists() || jarFile.exists()) {
      fileNo++;
      reFile = new File(backupPath+fileNo+".txt");
      jarFile = new File(backupPath+fileNo+".zip");
    }
    //更新备份日期
    long newLastTime = 
      (new SDate((new SDate()).getDayEndString())).getMillisecond();
    if (newLastTime!=lastTime) {
      fileNo = 0;
      lastTime = newLastTime;
    }
    return reFile;
  }

  /**
   * 获取当前时间
   * 刘虻
   * 2009-12-4 下午04:40:52
   * @return 当前时间
   */
  @Override
  public String now() {
    return simpleDateFormat.format(new Date());
  }

  /**
   * 设置输出到控制台日志编码格式
   * 刘虻
   * 2009-12-4 上午11:26:59
   * @param encoding 编码格式
   */
  public void setConsoleEncoding(String encoding) {
    this.consoleEncoding = encoding;
  }

  /**
   * 设置输出到文件日志编码格式
   * @param encoding 日志编码格式
   * 2020年8月20日
   * @author MBG
   */
  public void setFileEncoding(String encoding) {
    this.fileEncoding = encoding;
  }

  /**
   * 获取文件路径
   * 刘虻
   * 2009-12-4 下午05:19:41
   * @return 文件路径
   */
  public String getFilePath() {
    return filePath;
  }

  /**
   * 设置文件绝对路径
   * 刘虻
   * 2009-12-4 下午05:19:52
   * @param filePath 文件路径
   */
  public void setFilePath(String filePath) {
    this.filePath = filePath;
  }

  /**
   * 获取输出到控制台日志编码格式
   * 刘虻
   * 2009-12-4 上午11:26:35
   * @return 编码格式
   */
  @Override
  public String getConsoleEncoding() {
    return consoleEncoding;
  }

  /**
   * 获取写文件日志编码格式
   * @return 写文件日志编码格式
   * 2020年8月20日
   * @author MBG
   */
  @Override
  public String getFileEncoding() {
    return fileEncoding;
  }

  /**
   * 设置是否输出运行耗费时间日志
   * 刘虻
   * 2009-12-3 下午04:59:01
   * @param outRuntime 是否输出运行耗费时间日志
   */
  public void setOutRuntime(boolean outRuntime) {
    this.outRuntime = outRuntime;
  }

  /**
   * 是否输出耗费时间日志
   * 刘虻
   * 2010-2-1 下午04:24:17
   * @return 是否输出耗费时间日志
   */
  @Override
  public boolean isOutRuntime() {
    return outRuntime;
  }
  
  /**
   * 设置是否输出数据语句日志
   * 刘虻
   * 2009-12-3 下午04:57:49
   * @param outSql 是否输出数据语句日志
   */
  public void setOutSql(boolean outSql) {
    this.outSql = outSql;
  }
  
  /**
   * 是否写入系统错误日志
   * 刘虻
   * 2010-8-24 上午11:21:18
   * @return 是否写入系统错误日志
   */
  public boolean isWriteSystemError() {
    return writeSystemError;
  }
    
  /**
   * 设置是否写入系统错误日志
   * 刘虻
   * 2010-8-24 上午11:21:30
   * @param writeSystemError 是否写入系统错误日志
   */
  public void setWriteSystemError(boolean writeSystemError) {
    this.writeSystemError = writeSystemError;
  }
  
  /**
   * 是否写入系统普通日志
   * 刘虻
   * 2010-8-24 上午11:21:35
   * @return 是否写入系统普通日志
   */
  public boolean isWriteSystemLog() {
    return writeSystemLog;
  }
  
  /**
   * 设置是否写入系统普通日志
   * 刘虻
   * 2010-8-24 上午11:21:48
   * @param writeSystemLog 是否写入系统普通日志
   */
  public void setWriteSystemLog(boolean writeSystemLog) {
    this.writeSystemLog = writeSystemLog;
  }
  
  /**
   * 是否输出系统错误日志
   * 刘虻
   * 2010-8-24 上午11:19:15
   * @return 是否输出系统错误日志
   */
  public boolean isOutSystemError() {
    return outSystemError;
  }

  /**
   * 设置是否输出系统错误日志
   * 刘虻
   * 2010-8-24 上午11:19:29
   * @param outSystemError 是否输出系统错误日志
   */
  public void setOutSystemError(boolean outSystemError) {
    this.outSystemError = outSystemError;
  }

  /**
   * 是否输出系统普通日志
   * 刘虻
   * 2010-8-24 上午11:19:34
   * @return 是否输出系统普通日志
   */
  public boolean isOutSystemLog() {
    return outSystemLog;
  }
  
  /**
   * 设置是否输出系统普通日志
   * 刘虻
   * 2010-8-24 上午11:19:59
   * @param outSystemLog 是否输出系统普通日志
   */
  public void setOutSystemLog(boolean outSystemLog) {
    this.outSystemLog = outSystemLog;
  }
  
  /**
   * 设置是否输出启动日志
   * 刘虻
   * 2009-12-3 下午04:57:07
   * @param outStart 是否输出启动日志
   */
  public void setOutStart(boolean outStart) {
    this.outStart = outStart;
  }

  /**
   * 是否输出启动日志
   * 刘虻
   * 2010-2-1 下午04:20:43
   * @return 是否输出启动日志
   */
  @Override
  public boolean isOutStart() {
    return outStart;
  }

  /**
   * 设置是否输出错误日志
   * 刘虻
   * 2009-12-3 下午04:56:29
   * @param outError 是否输出错误日志
   */
  public void setOutError(boolean outError) {
    this.outError = outError;
  }

  /**
   * 是否输出错误日志
   * 刘虻
   * 2010-2-1 下午04:14:52
   * @return 是否输出错误日志
   */
  @Override
  public boolean isOutError() {
    return outError;
  }

  /**
   * 设置是否输出警告日志
   * 刘虻
   * 2009-12-3 下午04:55:56
   * @param outWarning 是否输出警告日志
   */
  public void setOutWarning(boolean outWarning) {
    this.outWarning = outWarning;
  }
  
  /**
   * 是否输出警告日志
   * 刘虻
   * 2010-2-1 下午04:22:46
   * @return 是否输出警告日志
   */
  @Override
  public boolean isOutWarning() {
    return outWarning;
  }
  
  /**
   * 设置是否输出info日志
   * @param outInfo 是否输出info日志
   * 2014年6月17日
   * @author 马宝刚
   */
  public void setOutInfo(boolean outInfo) {
    this.outInfo = outInfo;
  }

  /**
   * 是否输出info日志
   * @return 是否输出info日志
   * 2014年6月17日
   * @author 马宝刚
   */
  @Override
  public boolean isOutInfo() {
    return outInfo;
  }

  /**
   * 设置是否输出普通日志
   * 刘虻
   * 2009-12-3 下午04:55:22
   * @param outLog 是否输出普通日志
   */
  public void setOutLog(boolean outLog) {
    this.outLog = outLog;
  }

  /**
   * 设置是否输出交易数据日志
   * @param outData 是否输出交易数据日志
   * 2016年6月7日
   * @author MBG
   */
  @Override
  public void setOutData(boolean outData) {
    this.outData = outData;
  }

  /**
   * 是否输出普通日志
   * 刘虻
   * 2010-2-1 下午04:17:51
   * @return 是否输出普通日志
   */
  @Override
  public boolean isOutLog() {
    return outLog;
  }
  
  /**
   * 是否输出交易数据日志
   * @return 是否输出交易数据日志
   * 2016年6月7日
   * @author MBG
   */
  @Override
  public boolean isOutData() {
    return outData;
  }

  /**
   * 设置是否写入普通日志
   * 刘虻
   * 2009-12-4 下午05:25:09
   * @param writeLog 是否写入普通日志
   */
  public void setWriteLog(boolean writeLog) {
    this.writeLog = writeLog;
  }

  /**
   * 是否写入普通日志
   * 刘虻
   * 2010-2-1 下午04:18:42
   * @return 是否写入普通日志
   */
  @Override
  public boolean isWriteLog() {
    return writeLog;
  }

  /**
   * 是否写入交易数据日志
   * @return 是否写入交易数据日志
   * 2016年6月7日
   * @author MBG
   */
  @Override
  public boolean isWriteData() {
    return writeData;
  }

  /**
   * 设置是否写入警告日志
   * 刘虻
   * 2009-12-4 下午05:24:58
   * @param writeWarning 是否写入警告日志
   */
  public void setWriteWarning(boolean writeWarning) {
    this.writeWarning = writeWarning;
  }

  /**
   * 是否写入警告日志
   * 刘虻
   * 2010-2-1 下午04:23:26
   * @return 是否写入警告日志
   */
  @Override
  public boolean isWriteWarning() {
    return writeWarning;
  }

  /**
   * 设置是否写入info日志
   * @param writeInfo 是否写入info日志
   * 2014年6月17日
   * @author 马宝刚
   */
  public void setWriteInfo(boolean writeInfo) {
    this.writeInfo = writeInfo;
  }

  /**
   * 是否写入Info日志
   * @return 是否写入Info日志
   * 2014年6月17日
   * @author 马宝刚
   */
  @Override
  public boolean isWriteInfo() {
    return writeInfo;
  }

  /**
   * 设置是否写入错误日志
   * 刘虻
   * 2009-12-4 下午05:24:43
   * @param writeError 是否写入错误日志
   */
  public void setWriteError(boolean writeError) {
    this.writeError = writeError;
  }

  /**
   * 是否写入错误日志
   * 刘虻
   * 2010-2-1 下午04:16:15
   * @return 是否写入错误日志
   */
  @Override
  public boolean isWriteError() {
      return writeError;
  }

  /**
   * 设置是否写入启动日志
   * 刘虻
   * 2009-12-4 下午05:24:25
   * @param writeStart 是否写入启动日志
   */
  public void setWriteStart(boolean writeStart) {
    this.writeStart = writeStart;
  }

  /**
   * 是否写入启动日志
   * 刘虻
   * 2010-2-1 下午04:21:36
   * @return 是否写入启动日志
   */
  @Override
  public boolean isWriteStart() {
    return writeStart;
  }

  /**
   * 设置是否写入数据语句
   * 刘虻
   * 2009-12-4 下午05:23:45
   * @param writeSql 是否写入数据语句
   */
  public void setWriteSql(boolean writeSql) {
    this.writeSql = writeSql;
  }

  /**
   * 设置是否写入运行时间
   * 刘虻
   * 2009-12-4 下午05:23:33
   * @param writeRuntime 是否写入运行时间
   */
  public void setWriteRuntime(boolean writeRuntime) {
    this.writeRuntime = writeRuntime;
  }
  
  /**
   * 是否写入运行时间
   * 刘虻
   * 2010-2-1 下午04:25:11
   * @return 是否写入运行时间
   */
  @Override
  public boolean isWriteRuntime() {
    return writeRuntime;
  }

  /**
   * 覆盖方法
   * 刘虻
   * 2010-2-1 下午04:34:17
   */
  @Override
  public boolean isOutSql() {
    return outSql;
  }

  /**
   * 覆盖方法
   * 刘虻
   * 2010-2-1 下午04:34:31
   */
  @Override
  public boolean isWriteSql() {
    return writeSql;
  }


  /**
   * 覆盖方法
   * 刘虻
   * 2010-8-24 上午11:28:38
   */
  @Override
  public void write(byte[] buf, int off, int len) {
    getLogPrintStream().write(buf,off,len);
  }

  /**
   * 写入日志事件
   * @param content 日志内容
   * 2014年5月2日
   * @author 马宝刚
   */
  @Override
  public void writeLogEvent(String content) {
    if(content==null) {
      return;
    }
    leaVO.addLog(content);
  }

  /**
   * 是否将内部调试信息写入日志文件
   * （不赞成写入日志文件，因为信息量巨大）
   * 注意，debug 设计的初衷是临时查看显示信息
   * 并非后期打开日志查找之前的信息。 如果设置成
   * 写入文件，实际上跟随log方法，如果log方法没
   * 设置成写入文件，那么debug即使设置成写入文件，
   * 实际上也不会写入文件
   * @return 是否写入
   * 2015年6月18日
   * @author 马宝刚
   */
  @Override
  public boolean isWriteDebug() {
    return writeDebug;
  }

  /**
   * 设置是否将内部调试信息写入日志文件
   * @param writeInfo 是否将内部调试信息写入日志文件
   * 2014年6月17日
   * @author 马宝刚
   */
  public void setWriteDebug(boolean writeDebug) {
    this.writeDebug = writeDebug;
  }

  /**
   * 设置是否将交易数据写入独立的日志文件
   * @param writeData 是否将交易数据写入独立的日志文件
   * 2016年6月7日
   * @author MBG
   */
  @Override
  public void setWriteData(boolean writeData) {
    this.writeData = writeData;
  }

  /**
   * 覆盖方法
   */
  @Override
  public int getIndex() {
    return 9;
  }


  /**
   * 覆盖方法
   */
  @Override
  public String getFilterActionExtName() {
    return "ha";
  }

  /**
   * 覆盖方法
   */
  @Override
  public boolean doFilter(IRequestManager req, IResponseManager resp) throws Exception {
    if(!req.getServletPath().endsWith(servletPath)) {
      return false;
    }
    // 设置返回内容格式
    resp.setContentType("text/html; charset=UTF-8");
    // 获取请求陶肯
    String token = SString.valueOf(req.getUrlParameter("token"));
    String readPoint;  // 读取位置
    if(token.length()>0) {
      if(!callerMap.containsKey(token)) {
          // 获取输出流
          OutputStream respOs = resp.iGetOutputStream();
          if(respOs!=null) {
            // 发送重新获取陶肯命令
            respOs.write(("getToken('"+(cf==null?"":cf.getLocalServerInfoVO().name)+"');").getBytes());
          }
        return true;
      }
      // 刷新会话信息
      callerMap.put(token,callerOutTime+System.currentTimeMillis());
      // 取输出流
      OutputStream respOs = resp.iGetOutputStream();
      if(respOs==null) {
        return true;
      }
      /*
        * 注意：
        *       在页面中，获取直连服务器的日志，与通过直连服务器获取集群中其他服务器的日志，
        *       这两个动作在前台js中是分开执行的，避免获取直连服务器日志受获取集群日志时阻塞
        *       导致获取本地日志慢。 
        *       获取集群日志相比获取直连服务器日志慢是必然的，毕竟需要中转。
        */
      if(SBoolean.valueOf(req.getUrlParameter("_o"))) {
        if(cf==null) {
          if(!_beanFactory.beanExists(ClusterFilter.class)) {
            respOs.write("不支持集群获取日志功能，请检查配置文件配置。".getBytes(StandardCharsets.UTF_8));
            return true;
          }
          try {
            cf = _beanFactory.getObject(ClusterFilter.class,this);
            cf.regist(this); //注册当前类
          }catch(Exception e) {
            e.printStackTrace();
            respOs.write(("获取集群服务发生异常:["+e+"]").getBytes(StandardCharsets.UTF_8));
            return true;
          }
        }
        //调用服务器返回值
        String res;
        //获取集群服务器信息序列
        List<ServerInfoVO> siVOList = cf.getServerInfoList(groupName);
        for(ServerInfoVO ele:siVOList) {
          //已弃用，请看变量ServerInfoVO.invalidServer声明处的注释
          //if(ele.local || ele.disabled || ele.invalidServer || !ele.validSend) {
          if(ele.local || ele.disabled || !ele.validSend) {
            continue;
          }
          readPoint = req.getUrlParameter("_"+ele.name);
          try {
            res = cf.callAction(ele.name,servletPath+"?cluster_log=1&readPoint="+readPoint,null);
          }catch(Exception e) {
            System.out.println("-------Cluster LOG:"+ele.name+":"+e.toString());
            continue;
          }
          if(res!=null && res.length()>0) {
            respOs.write(res.getBytes());
            respOs.write('\n');
          }
        }
        return true;
      }
      readPoint = req.getUrlParameter("readPoint");
      respOs.write(leaVO.getLog(readPoint,true).getBytes());
    }else if(SBoolean.valueOf(req.getUrlParameter("cluster_log"))) {
      //由同组集群中的服务器发起获取日志内容
      if(cf==null || !cf.allow(req)) {
        return true;
      }
      //获取输出流
      OutputStream respOs = resp.iGetOutputStream();
      readPoint = req.getUrlParameter("readPoint");
      respOs.write(leaVO.getLog(readPoint,false).getBytes());
    }
    return true;
  }

  /**
   * 覆盖方法
   */
  @Override
  public void init(Object caller) throws Exception {
    cf                = (ClusterFilter)caller;
    ServerInfoVO siVO = cf.getLocalServerInfoVO(); //获取当前服务器信息
    if(siVO==null) {
      return;
    }
    serverName        = cf.name();
    groupName         = cf.group();
    leaVO.setCluster(serverName,groupName); //设置当前集群信息
  }

  /**
   * 请求查看实施日志
   * @param token 之前用的token，如果已经过期，则分配一个新的值，如果没过期，还用这个值
   * @return 查看日志的token
   * 2018年7月12日
   * @author MBG
   */
  @Override
    public synchronized String getToken(String token) {
    //用当前毫秒值做会话主键
    if(token==null || token.length()<1 || !callerMap.containsKey(token)) {
      token = String.valueOf(System.currentTimeMillis());
      while(callerMap.containsKey(token)) {
        try {
          Thread.sleep(10);
        }catch(Exception e) {}
        token = String.valueOf(System.currentTimeMillis());
      }
      callerMap.put(token,callerOutTime+System.currentTimeMillis());
    }else {
      //刷新会话信息
      callerMap.put(token,callerOutTime+System.currentTimeMillis());
    }
    return token;
  }
  
  /**
   * 是否输出临时日志（测试后需要删除的日志）
   * @return 是否输出临时日志
   * 2018年7月17日
   * @author MBG
   */
  @Override
  public boolean isOutTLog() {
    return outTLog;
  }
  
  /**
   * 设置是否输出临时日志 （测试后需要删除的日志）
   * @param outTLog 是否输出临时日志
   * 2018年7月17日
   * @author MBG
   */
  @Override
  public void setOutTLog(boolean outTLog) {
    this.outTLog = outTLog;
  }
  
  /**
   * 是否需要写入临时日志到文件（测试后需要删除的日志）
   * @return 是否需要写入临时日志到文件
   * 2018年7月17日
   * @author MBG
   */
  @Override
  public boolean isWriteTLog() {
    return writeTLog;
  }
  
  /**
   * 设置是否需要写入临时日志到文件 （测试后需要删除的日志）
   * @param writeTLog 是否需要写入临时日志到文件
   * 2018年7月17日
   * @author MBG
   */
  @Override
  public void setWriteTLog(boolean writeTLog) {
    this.writeTLog = writeTLog;
  }
  
  /**
   * 从配置文件中获取到允许自定义的日志文件头（不包含扩展名）
   * @param fileKeys 日志文件头（不包含扩展名），多个用逗号分隔
   * 2018年7月30日
   * @author MBG
   */
  public void addCustomLogFileKeys(String fileKeys) {
    List<String> subKeyList = BaseUtil.splitToList(fileKeys,",",";","，","；");
    for(String subKey:subKeyList) {
      subKey = subKey.trim();
      if(subKey.length()<1 
        || logFileKeyList.contains(subKey) 
        || "log".equalsIgnoreCase(subKey)
        || subKey.toLowerCase().startsWith("data_")) {
        continue;
      }
      logFileKeyList.add(subKey);
    }
  }

  /**
   * 设置故障码
   * @param code  故障码
   * 2018年7月17日
   * @author MBG
   */
  @Override
  public void setAlertCode(String code) {
	  long ts = System.currentTimeMillis(); //当前时间
	  if(alertCodeList.contains(code)) {
		  alertAfterTimeMap.put(code,ts);
		  long count = alertCountMap.get(code);
		  if(++count>=Long.MAX_VALUE) {count=0;}
		  alertCountMap.put(code,count);
	  }else {
		  alertCodeList.add(code);
		  alertBeforeTimeMap.put(code,ts);
		  alertAfterTimeMap.put(code,ts);
		  alertCountMap.put(code,1L);
	  }
  }
}